package com.dragon.common.utils.io.code;

import com.dragon.common.utils.StringUtils;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.Result;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import lombok.Data;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

/**
 * 二维码工具类
 *
 * @author dragon
 * @version 1.0
 * @date 2024/8/12 16:25
 */
public class QrCodeUtils {

    private static final Logger log = LoggerFactory.getLogger(QrCodeUtils.class);

    @Data
    public static class Config {
        /**
         * 二维码宽度，单位像素
         */
        private int codeWidth = 300;

        /**
         * 二维码高度，单位像素
         */
        private int codeHeight = 300;

        /**
         * 二维码前景色，0x000000 表示黑色
         */
        private Color frontColor = Color.BLACK;

        /**
         * 二维码背景色，0xFFFFFF 表示白色
         */
        private Color backgroundColor = Color.WHITE;

        /**
         * logo宽度，单位像素
         */
        private int logoWidth = 60;

        /**
         * logo高度，单位像素
         */
        private int logoHeight = 60;

        /**
         * 纠错级别
         * ErrorCorrectionLevel：误差校正等级，L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
         * 不设置时，默认为 L 等级，等级不一样，生成的图案不同，但扫描的结果是一样的
         */
        private ErrorCorrectionLevel errorCorrection = ErrorCorrectionLevel.M;

        /**
         * 设置边距，既二维码和背景之间的边距
         */
        private int margin = 1;

        /**
         * logo图片
         */
        private String logoPic;

        public Config() {
        }

        public Config(int codeWidth, int codeHeight) {
            this.codeWidth = codeWidth;
            this.codeHeight = codeHeight;
        }

    }

    /**
     * 生成二维码
     *
     * @param content    二维码内容
     * @param targetFile 二维码文件
     * @throws WriterException 创建二维码失败
     * @throws IOException     写入二维码文件失败
     */
    public static void generate(String content, File targetFile) throws WriterException, IOException {
        BufferedImage bufferedImage = generate(new Config(), content);

        ImageIO.write(bufferedImage, FilenameUtils.getExtension(targetFile.getName()), targetFile);
    }

    /**
     * 生成二维码
     *
     * @param content      二维码内容
     * @param outputStream 二维码输出流
     * @throws WriterException 创建二维码失败
     * @throws IOException     写入二维码文件失败
     */
    public static void generate(String content, OutputStream outputStream) throws WriterException, IOException {
        BufferedImage bufferedImage = generate(new Config(), content);

        ImageIO.write(bufferedImage, "png", outputStream);
    }

    /**
     * 生成二维码
     *
     * @param content    二维码内容
     * @param targetFile 二维码文件
     * @param codeWidth  二维码宽度
     * @param codeHeight 二维码高度
     * @throws WriterException 创建二维码失败
     * @throws IOException     写入二维码文件失败
     */
    public static void generate(String content, File targetFile, int codeWidth, int codeHeight) throws WriterException, IOException {
        BufferedImage bufferedImage = generate(new Config(codeWidth, codeHeight), content);

        ImageIO.write(bufferedImage, FilenameUtils.getExtension(targetFile.getName()), targetFile);
    }

    /**
     * 生成二维码
     *
     * @param content      二维码内容
     * @param outputStream 二维码输出流
     * @param codeWidth    二维码宽度
     * @param codeHeight   二维码高度
     * @throws WriterException 创建二维码失败
     * @throws IOException     写入二维码文件失败
     */
    public static void generate(String content, OutputStream outputStream, int codeWidth, int codeHeight) throws WriterException, IOException {
        BufferedImage bufferedImage = generate(new Config(codeWidth, codeHeight), content);

        ImageIO.write(bufferedImage, "png", outputStream);
    }

    /**
     * 生成二维码
     *
     * @param config     二维码配置
     * @param content    二维码内容
     * @param targetFile 二维码文件
     * @throws WriterException 创建二维码失败
     * @throws IOException     写入二维码文件失败
     */
    public static void generate(Config config, String content, File targetFile) throws WriterException, IOException {
        BufferedImage bufferedImage = generate(config, content);

        ImageIO.write(bufferedImage, FilenameUtils.getExtension(targetFile.getName()), targetFile);
    }

    /**
     * 生成二维码
     *
     * @param config       二维码配置
     * @param content      二维码内容
     * @param outputStream 二维码文件输出流
     * @throws WriterException 创建二维码失败
     * @throws IOException     写入二维码文件失败
     */
    public static void generate(Config config, String content, OutputStream outputStream) throws WriterException, IOException {
        BufferedImage bufferedImage = generate(config, content);

        ImageIO.write(bufferedImage, "png", outputStream);
    }

    /**
     * 生成二维码
     *
     * @param config  二维码配置
     * @param content 二维码内容
     * @return 二维码图片
     * @throws WriterException 创建二维码失败
     * @throws IOException     写入二维码文件失败
     */
    public static BufferedImage generate(Config config, String content) throws WriterException, IOException {

        /*
         * com.google.zxing.EncodeHintType：编码提示类型,枚举类型
         * EncodeHintType.CHARACTER_SET：设置字符编码类型
         * EncodeHintType.ERROR_CORRECTION：设置误差校正
         *      ErrorCorrectionLevel：误差校正等级，L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
         *      不设置时，默认为 L 等级，等级不一样，生成的图案不同，但扫描的结果是一样的
         * EncodeHintType.MARGIN：设置二维码边距，单位像素，值越小，二维码距离四周越近
         */
        Map<EncodeHintType, Object> hints = new HashMap<>(3);
        hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8);
        hints.put(EncodeHintType.ERROR_CORRECTION, config.getErrorCorrection());
        hints.put(EncodeHintType.MARGIN, config.getMargin());

        /*
         * MultiFormatWriter:多格式写入，这是一个工厂类，里面重载了两个 encode 方法，用于写入条形码或二维码
         *      encode(String contents,BarcodeFormat format,int width, int height,Map<EncodeHintType,?> hints)
         *      contents:条形码/二维码内容
         *      format：编码类型，如 条形码，二维码 等
         *      width：码的宽度
         *      height：码的高度
         *      hints：码内容的编码类型
         * BarcodeFormat：枚举该程序包已知的条形码格式，即创建何种码，如 1 维的条形码，2 维的二维码 等
         * BitMatrix：位(比特)矩阵或叫2D矩阵，也就是需要的二维码
         */
        MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
        BitMatrix bitMatrix = multiFormatWriter.encode(content, BarcodeFormat.QR_CODE, config.codeWidth, config.codeHeight, hints);

        /*
         * java.awt.image.BufferedImage：具有图像数据的可访问缓冲图像，实现了 RenderedImage 接口
         * BitMatrix 的 get(int x, int y) 获取比特矩阵内容，指定位置有值，则返回true，将其设置为前景色，否则设置为背景色
         * BufferedImage 的 setRGB(int x, int y, int rgb) 方法设置图像像素
         *      x：像素位置的横坐标，即列
         *      y：像素位置的纵坐标，即行
         *      rgb：像素的值，采用 16 进制,如 0xFFFFFF 白色
         */
        final BufferedImage bufferedImage = drawQrCode(config, bitMatrix);

        // 绘制logo
        drawLogo(config, bufferedImage);

        return bufferedImage;
    }

    /**
     * 绘制二维码
     *
     * @param config    二维码配置
     * @param bitMatrix 二维码矩阵
     * @return 二维码图片
     */
    private static @NonNull BufferedImage drawQrCode(Config config, BitMatrix bitMatrix) {
        BufferedImage bufferedImage = new BufferedImage(config.getCodeWidth(), config.getCodeHeight(), BufferedImage.TYPE_INT_BGR);
        for (int x = 0; x < config.getCodeWidth(); x++) {
            for (int y = 0; y < config.getCodeHeight(); y++) {
                bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? config.frontColor.getRGB() : config.getBackgroundColor().getRGB());
            }
        }
        return bufferedImage;
    }

    /**
     * 绘制logo
     *
     * @param config        二维码配置
     * @param bufferedImage 二维码图像
     * @throws IOException 读取文件异常
     */
    private static void drawLogo(Config config, BufferedImage bufferedImage) throws IOException {
        if (StringUtils.isBlank(config.getLogoPic())) {
            return;
        }

        File logoFile = new File(config.getLogoPic());
        if (!logoFile.exists()) {
            return;
        }

        Image logoImage = ImageIO.read(logoFile);
        BufferedImage logoBufferedImage = new BufferedImage(config.getLogoWidth(), config.getLogoHeight(), BufferedImage.TYPE_INT_RGB);
        Graphics graphics = logoBufferedImage.getGraphics();
        // 绘制缩小后的图
        graphics.drawImage(logoImage, 0, 0, null);
        graphics.dispose();

        Graphics2D graphics2D = bufferedImage.createGraphics();

        int x = (config.getCodeWidth() - config.getLogoWidth()) / 2;
        int y = (config.getCodeHeight() - config.getLogoHeight()) / 2;

        // 绘制logo
        graphics2D.drawImage(logoImage, x, y, config.getLogoWidth(), config.getLogoHeight(), null);

        // 圆角的矩形
        Shape shape = new RoundRectangle2D.Float(x, y, config.getLogoWidth(), config.getLogoHeight(), 6, 6);
        // 设置笔画对象
        graphics2D.setStroke(new BasicStroke(3f));
        graphics2D.draw(shape);

        graphics2D.dispose();
    }

    /**
     * 解析二维码
     *
     * @param file 二维码文件
     * @return 二维码内容
     */
    public static String decode(File file) {
        if (file == null || !file.exists()) {
            return null;
        }

        try {
            /*
             * ImageIO 的 BufferedImage read(URL input) 方法用于读取网络图片文件转为内存缓冲图像
             * 同理还有：read(File input)、read(InputStream input)、、read(ImageInputStream stream)
             */
            BufferedImage bufferedImage = ImageIO.read(file);

            return decode(bufferedImage);
        } catch (Exception e) {
            log.error("读取二维码失败 {}", e.getMessage(), e);
        }

        return null;
    }

    /**
     * 解析二维码
     *
     * @param url 二维码链接
     * @return 二维码内容
     */
    public static String decode(URL url) {
        if (url == null) {
            return null;
        }

        try {
            /*
             * ImageIO 的 BufferedImage read(URL input) 方法用于读取网络图片文件转为内存缓冲图像
             * 同理还有：read(File input)、read(InputStream input)、、read(ImageInputStream stream)
             */
            BufferedImage bufferedImage = ImageIO.read(url);

            return decode(bufferedImage);
        } catch (Exception e) {
            log.error("读取二维码失败 {}", e.getMessage(), e);
        }

        return null;
    }

    /**
     * 解析二维码
     *
     * @param bufferedImage 二维码图像
     * @return 二维码内容
     */
    public static String decode(BufferedImage bufferedImage) {
        if (bufferedImage == null) {
            return null;
        }

        try {
            /*
             * com.google.zxing.client.j2se.BufferedImageLuminanceSource：缓冲图像亮度源
             * 将 java.awt.image.BufferedImage 转为 zxing 的 缓冲图像亮度源
             * 关键就是下面这几句：HybridBinarizer 用于读取二维码图像数据，BinaryBitmap 二进制位图
             */
            BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
            BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
            Hashtable<DecodeHintType, Object> hints = new Hashtable<>(1);
            hints.put(DecodeHintType.CHARACTER_SET, StandardCharsets.UTF_8);

            /*
             * 如果图片不是二维码图片，则 decode 抛异常：com.google.zxing.NotFoundException
             * MultiFormatWriter 的 encode 用于对内容进行编码成 2D 矩阵
             * MultiFormatReader 的 decode 用于读取二进制位图数据
             */
            Result result = new MultiFormatReader().decode(bitmap, hints);
            return result.getText();
        } catch (Exception e) {
            log.error("读取二维码失败 {}", e.getMessage(), e);
        }

        return null;
    }

}
